import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:gim_plugin/src/podo/FileContentType.dart';
import 'package:gim_plugin/src/podo/GenCosUploadUrlDto.dart';
import 'package:gim_plugin/src/podo/GetArtcTokenDto.dart';
import 'package:gim_plugin/src/podo/GetTrtcTokenDto.dart';
import 'package:gim_plugin/src/podo/HttpResult.dart';
import 'package:gim_plugin/src/podo/MsgDto.dart';
import 'package:web_socket_channel/io.dart';

import '../gim_plugin.dart';

typedef OnReceiveMsg = Future<void> Function(MsgDto);

typedef GimError = void Function({String? code, String? msg});

class Gim {
  bool _shutdown = false;
  bool _debug = true;
  String? _uid;
  bool _ssl = true;
  String? _domain;
  String? _token;
  String? _platform;
  String? _appId;
  IOWebSocketChannel? _channel;
  final HttpClient _httpClient = HttpClient();

  Timer? timer;

  Gim({
    bool ssl = true,
    bool debug = true,
    required String uid,
    required String domain,
    required String token,
    required String appId,
    required String platform,
  }) {
    _ssl = ssl;
    _debug = debug;
    _uid = uid;
    _domain = domain;
    _token = token;
    _appId = appId;
    _platform = platform;
    _init();
  }

  void _init() {
    _shutdown = false;
    timer?.cancel();
    _channel?.sink.close();
    _channel = IOWebSocketChannel.connect('${_ssl ? 'wss' : 'ws'}://$_domain/gim/ws/socket',
        headers: {"appId": _appId, "platform": _platform, "token": _token}, pingInterval: const Duration(seconds: 20));
    _channel?.stream.listen((event) {
      var msg = json.decode(event);
      _dispatcher(msg);
      infoLog(msg);
    }, onError: (err, stack) {
      errLog("\n$err\n$stack");
      // _reconnect();
    }, onDone: () {
      infoLog("GIM断开了");
      _reconnect();
    });
    timer = Timer.periodic(const Duration(seconds: 5), (_) {
      _channel?.sink.add("{}");
    });
  }

  void _reconnect() {
    if (!_shutdown) {
      Future.delayed(const Duration(seconds: 5)).then((_) {
        infoLog("GIM开始重新连接");
        _init();
      });
    }
  }

  void dispose() {
    _shutdown = true;
    timer?.cancel();
    _channel?.sink.close();
  }

  void _dispatcher(dynamic msg) {
    switch (msg['event']) {
      case EventTypes.SybConversationUpdate:
        for (var fn in _conversationUpdateFn) {
          fn(ConversationInfo.fromJson(msg['data']));
        }
        break;
      case EventTypes.SybNewC2cMsg:
        for (var fn in _newC2cMsgFn) {
          fn(C2cMsg.fromJson(msg['data']));
        }
        break;
      case EventTypes.SybEditC2cMsg:
        for (var fn in _editC2cMsgFn) {
          fn(C2cMsg.fromJson(msg['data']));
        }
        break;
      case EventTypes.SybUnreadCount:
        for (var fn in _unreadCountChangeFn) {
          fn(msg['data']);
        }
        break;
      case EventTypes.SybWithdrawC2cMsg:
        for (var fn in _withdrawC2cMsgFn) {
          fn(msg['data']);
        }
        break;
      case EventTypes.SybMarkMsgAsRead:
        for (var fn in _markMsgAsReadFn) {
          fn(msg['data']);
        }
        break;
      case EventTypes.SignalingInvite:
      case EventTypes.SignalingCancel:
      case EventTypes.SignalingAccept:
      case EventTypes.SignalingReject:
      case EventTypes.SignalingTimeout:
        for (var fn in _signalingFn) {
          var data = SignalingMsg.fromJson(msg['data']);
          data.event = msg['event'];
          fn(data);
        }
        break;
      default:
        break;
    }
  }

  final Set<ValueChanged<ConversationInfo>> _conversationUpdateFn = {};

  void addConversationUpdateFn(ValueChanged<ConversationInfo> fn) {
    _conversationUpdateFn.add(fn);
  }

  void delConversationUpdateFn(ValueChanged<ConversationInfo> fn) {
    _conversationUpdateFn.remove(fn);
  }

  final Set<ValueChanged<C2cMsg>> _newC2cMsgFn = {};

  void addNewC2cMsgFn(ValueChanged<C2cMsg> fn) {
    _newC2cMsgFn.add(fn);
  }

  void delNewC2cMsgFn(ValueChanged<C2cMsg> fn) {
    _newC2cMsgFn.remove(fn);
  }

  final Set<ValueChanged<C2cMsg>> _editC2cMsgFn = {};

  void addEditC2cMsgFn(ValueChanged<C2cMsg> fn) {
    _editC2cMsgFn.add(fn);
  }

  void delEditC2cMsgFn(ValueChanged<C2cMsg> fn) {
    _editC2cMsgFn.remove(fn);
  }

  final Set<ValueChanged<int>> _unreadCountChangeFn = {};

  void addUnreadCountChangeFn(ValueChanged<int> fn) {
    _unreadCountChangeFn.add(fn);
  }

  void delUnreadCountChangeFn(ValueChanged<int> fn) {
    _unreadCountChangeFn.remove(fn);
  }

  final Set<ValueChanged<int>> _withdrawC2cMsgFn = {};

  void addWithdrawC2cMsgFn(ValueChanged<int> fn) {
    _withdrawC2cMsgFn.add(fn);
  }

  void delWithdrawC2cMsgFn(ValueChanged<int> fn) {
    _withdrawC2cMsgFn.remove(fn);
  }

  final Set<ValueChanged<String>> _markMsgAsReadFn = {};

  void addMarkMsgAsReadFn(ValueChanged<String> fn) {
    _markMsgAsReadFn.add(fn);
  }

  void delMarkMsgAsReadFn(ValueChanged<String> fn) {
    _markMsgAsReadFn.remove(fn);
  }

  final Set<ValueChanged<SignalingMsg>> _signalingFn = {};

  void addSignalingFn(ValueChanged<SignalingMsg> fn) {
    _signalingFn.add(fn);
  }

  void delSignalingFn(ValueChanged<SignalingMsg> fn) {
    _signalingFn.remove(fn);
  }

  Future<C2cMsg?> sendTextMsg(
      {required String toUid,
      required String text,
      bool notPush = false,
      String? pushTitle,
      String? pushContent,
      ValueChanged<C2cMsg>? placeholder,
      GimError? gimError}) async {
    var preId = '${DateTime.now().microsecondsSinceEpoch}';
    if (placeholder != null) {
      placeholder(C2cMsg(
          preId: preId,
          fromUid: _uid,
          toUid: toUid,
          msgType: MsgTypes.MsgTypeText,
          msgData: text,
          readed: false,
          delUid: '0',
          createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000));
    }
    var dto = await _ajax(
        path: '/gim/c2c/sendC2cMsg',
        body: {
          'to_uid': toUid,
          'pre_id': preId,
          'msg_type': MsgTypes.MsgTypeText,
          'msg_data': text,
          'not_push': notPush,
          'push_title': pushTitle,
          'push_content': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsg.fromJson(dto.data);
  }

  Future<C2cMsg?> sendImageMsg(
      {required String toUid,
      required ImageMsg msg,
      bool notPush = false,
      String? pushTitle,
      String? pushContent,
      ValueChanged<C2cMsg>? placeholder,
      GimError? gimError}) async {
    var preId = '${DateTime.now().microsecondsSinceEpoch}';
    if (placeholder != null) {
      placeholder(C2cMsg(
          preId: preId,
          fromUid: _uid,
          toUid: toUid,
          msgType: MsgTypes.MsgTypeImage,
          msgData: json.encode(msg.toJson()),
          readed: false,
          delUid: '0',
          createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000));
    }
    var key = await uploadFile(filePath: msg.url!);
    if (key == null) return null;
    msg.url = key;
    var dto = await _ajax(
        path: '/gim/c2c/sendC2cMsg',
        body: {
          'to_uid': toUid,
          'pre_id': preId,
          'msg_type': MsgTypes.MsgTypeImage,
          'msg_data': json.encode(msg.toJson()),
          'not_push': notPush,
          'push_title': pushTitle,
          'push_content': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsg.fromJson(dto.data);
  }

  Future<C2cMsg?> sendVideoMsg(
      {required String toUid,
      required VideoMsg msg,
      bool notPush = false,
      String? pushTitle,
      String? pushContent,
      ValueChanged<C2cMsg>? placeholder,
      GimError? gimError}) async {
    var preId = '${DateTime.now().microsecondsSinceEpoch}';
    if (placeholder != null) {
      placeholder(C2cMsg(
          preId: preId,
          fromUid: _uid,
          toUid: toUid,
          msgType: MsgTypes.MsgTypeVideo,
          msgData: json.encode(msg.toJson()),
          readed: false,
          delUid: '0',
          createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000));
    }
    var key = await uploadFile(filePath: msg.url!);
    if (key == null) return null;
    await Future.delayed(const Duration(seconds: 3));
    msg.url = key;
    msg.cover = '${key.split('.')[0]}_0.jpg';
    var dto = await _ajax(
        path: '/gim/c2c/sendC2cMsg',
        body: {
          'to_uid': toUid,
          'pre_id': preId,
          'msg_type': MsgTypes.MsgTypeVideo,
          'msg_data': json.encode(msg.toJson()),
          'not_push': notPush,
          'push_title': pushTitle,
          'push_content': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;

    return C2cMsg.fromJson(dto.data);
  }

  Future<C2cMsg?> sendAudioMsg(
      {required String toUid,
      required AudioMsg msg,
      bool notPush = false,
      String? pushTitle,
      String? pushContent,
      ValueChanged<C2cMsg>? placeholder,
      GimError? gimError}) async {
    var preId = '${DateTime.now().microsecondsSinceEpoch}';
    if (placeholder != null) {
      placeholder(C2cMsg(
          preId: preId,
          fromUid: _uid,
          toUid: toUid,
          msgType: MsgTypes.MsgTypeAudio,
          msgData: json.encode(msg.toJson()),
          readed: false,
          delUid: '0',
          createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000));
    }
    var key = await uploadFile(filePath: msg.url!);
    if (key == null) return null;
    msg.url = key;
    var dto = await _ajax(
        path: '/gim/c2c/sendC2cMsg',
        body: {
          'to_uid': toUid,
          'pre_id': preId,
          'msg_type': MsgTypes.MsgTypeAudio,
          'msg_data': json.encode(msg.toJson()),
          'not_push': notPush,
          'push_title': pushTitle,
          'push_content': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsg.fromJson(dto.data);
  }

  Future<C2cMsg?> sendGeoMsg(
      {required String toUid,
      required GeoMsg msg,
      bool notPush = false,
      String? pushTitle,
      String? pushContent,
      ValueChanged<C2cMsg>? placeholder,
      GimError? gimError}) async {
    var preId = '${DateTime.now().microsecondsSinceEpoch}';
    if (placeholder != null) {
      placeholder(C2cMsg(
          preId: preId,
          fromUid: _uid,
          toUid: toUid,
          msgType: MsgTypes.MsgTypeGeo,
          msgData: json.encode(msg.toJson()),
          readed: false,
          delUid: '0',
          createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000));
    }
    var dto = await _ajax(
        path: '/gim/c2c/sendC2cMsg',
        body: {
          'to_uid': toUid,
          'pre_id': preId,
          'msg_type': MsgTypes.MsgTypeGeo,
          'msg_data': json.encode(msg.toJson()),
          'not_push': notPush,
          'push_title': pushTitle,
          'push_content': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsg.fromJson(dto.data);
  }

  Future<C2cMsg?> sendCustomMsg(
      {required String toUid,
      required String msg,
      required String pushTitle,
      required String pushContent,
      ValueChanged<C2cMsg>? placeholder,
      GimError? gimError}) async {
    var preId = '${DateTime.now().microsecondsSinceEpoch}';
    if (placeholder != null) {
      placeholder(C2cMsg(
          preId: preId,
          fromUid: _uid,
          toUid: toUid,
          msgType: MsgTypes.MsgTypeCustom,
          msgData: msg,
          readed: false,
          delUid: '0',
          createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000));
    }
    var dto = await _ajax(
        path: '/gim/c2c/sendC2cMsg',
        body: {
          'to_uid': toUid,
          'pre_id': preId,
          'msg_type': MsgTypes.MsgTypeCustom,
          'msg_data': msg,
          'push_title': pushTitle,
          'push_content': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsg.fromJson(dto.data);
  }

  Future<bool> delC2cMsg({String? toUid, int? msgId, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/c2c/delC2cMsg',
        body: {
          'to_uid': toUid,
          'msg_id': msgId,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data ?? false;
  }

  Future<C2cMsg?> editC2cMsg({required int msgId, required String msgData, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/c2c/editC2cMsg',
        body: {
          'msg_id': msgId,
          'msg_data': msgData,
        },
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsg.fromJson(dto.data);
  }

  Future<C2cMsgList?> getC2cMsgList({required String toUid, int? lastId, int? pageSize, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/c2c/getC2cMsgList',
        body: {'to_uid': toUid, 'last_id': lastId, 'page_size': pageSize},
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsgList.fromJson(dto.data);
  }

  Future<int?> getUnreadCount({String? fromUid, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/c2c/getUnreadCount',
        body: {
          'from_uid': fromUid,
        },
        gimError: gimError);
    if (dto == null) return null;
    return dto.data;
  }

  Future<bool> markC2cMsgRead({String? fromUid, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/c2c/markC2cMsgRead',
        body: {
          'from_uid': fromUid,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<C2cMsg?> sendC2cMsg(
      {required String toUid,
      required dynamic msg,
      required String msgType,
      bool notPush = false,
      String? pushTitle,
      String? pushContent,
      ValueChanged<C2cMsg>? placeholder,
      GimError? gimError}) async {
    var preId = '${DateTime.now().microsecondsSinceEpoch}';
    if (placeholder != null) {
      placeholder(C2cMsg(
          preId: preId,
          fromUid: _uid,
          toUid: toUid,
          msgType: msgType,
          msgData: msg,
          readed: false,
          delUid: '0',
          createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000));
    }
    var dto = await _ajax(
        path: '/gim/c2c/sendC2cMsg',
        body: {
          'to_uid': toUid,
          'pre_id': preId,
          'msg_type': msgType,
          'msg_data': msg,
          'push_title': pushTitle,
          'push_content': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;
    return C2cMsg.fromJson(dto.data);
  }

  Future<bool> withdrawC2cMsg({required int msgId, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/c2c/withdrawC2cMsg',
        body: {
          'msg_id': msgId,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<bool> resetOnFrontDesk({required bool flag, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/com/resetOnFrontDesk',
        body: {
          'flag': flag,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<int?> delConversation({required int conversationId, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/conversation/delConversation',
        body: {
          'conversation_id': conversationId,
        },
        gimError: gimError);
    if (dto == null) return null;
    return dto.data;
  }

  Future<ConversationList?> getConversationList({int? pageNo, int? pageSize, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/conversation/getConversationList',
        body: {
          'page_no': pageNo,
          'page_size': pageSize,
        },
        gimError: gimError);
    if (dto == null) return null;
    return ConversationList.fromJson(dto.data);
  }

  Future<bool> checkMarkTop({int? conversationId, String? toUid, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/conversation/checkMarkTop',
        body: {
          'conversation_id': conversationId,
          'to_uid': toUid,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<bool> markConversationTop({int? conversationId, String? toUid, required bool markTop, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/conversation/markConversationTop',
        body: {
          'conversation_id': conversationId,
          'to_uid': toUid,
          'mark_top': markTop,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<bool> signalingAccept({required int signalingId, dynamic extData, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/signaling/accept',
        body: {
          'signaling_id': signalingId,
          'ext_data': extData,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<bool> signalingCancel(
      {required int signalingId,
      dynamic extData,
      required String pushTitle,
      required String pushContent,
      GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/signaling/cancel',
        body: {
          'signaling_id': signalingId,
          'ext_data': extData,
          'push_title': pushTitle,
          'push_account': pushContent,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<int?> signalingInvite(
      {required String uid,
      dynamic extData,
      required String pushTitle,
      required String pushContent,
      GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/signaling/invite',
        body: {
          'uid': uid,
          'ext_data': extData,
          'push_title': pushTitle,
          'push_account': pushContent,
        },
        gimError: gimError);
    if (dto == null) return null;
    return dto.data;
  }

  Future<bool> signalingReject({required int signalingId, dynamic extData, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/signaling/reject',
        body: {
          'signaling_id': signalingId,
          'ext_data': extData,
        },
        gimError: gimError);
    if (dto == null) return false;
    return dto.data;
  }

  Future<void> checkLastSignaling({GimError? gimError}) async {
    _ajax(path: '/gim/signaling/checkLastSignaling', gimError: gimError);
  }

  Future<GetTrtcTokenDto?> getTrtcToken({GimError? gimError}) async {
    var dto = await _ajax(path: '/gim/rtc/getTrtcToken', gimError: gimError);
    if (dto == null) return null;
    return GetTrtcTokenDto.fromJson(dto.data);
  }

  Future<GetArtcTokenDto?> getArtcToken(
      {required int uid, required String channelName, int role = 1, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/rtc/getArtcToken',
        body: {'uid': uid, 'channel_name': channelName, 'role': role},
        gimError: gimError);
    if (dto == null) return null;
    return GetArtcTokenDto.fromJson(dto.data);
  }

  Future<GenCosUploadUrlDto?> genCosUploadUrl(
      {required String prefix, required String mime, GimError? gimError}) async {
    var dto = await _ajax(
        path: '/gim/com/genCosUploadUrl',
        body: {
          'prefix': prefix,
          'mime': mime,
        },
        gimError: gimError);
    if (dto == null) return null;
    return GenCosUploadUrlDto.fromJson(dto.data);
  }

  Future<String?> uploadFile({required String filePath, GimError? gimError}) async {
    var arr = filePath.split('.');
    String mime = arr[arr.length - 1];
    var contentType = fileContentType[mime.toLowerCase()];
    String prefix = contentType!.split('/')[0];
    var dto = await genCosUploadUrl(prefix: prefix, mime: mime, gimError: gimError);
    if (dto == null) {
      return null;
    }
    File file = File(filePath);
    infoLog(file.lengthSync());
    var request = await _httpClient.putUrl(Uri.parse(dto.url!));
    request.headers.set(HttpHeaders.contentTypeHeader, contentType);
    // 读文件
    var s = await file.open();
    var x = 0;
    var size = file.lengthSync();
    request.headers.set(HttpHeaders.contentLengthHeader, size);
    var chunkSize = 65536;
    while (x < size) {
      var _len = size - x >= chunkSize ? chunkSize : size - x;
      var val = s.readSync(_len).toList();
      x = x + _len;
      // 加入http发送缓冲区
      request.add(val);
      // 立即发送并清空缓冲区
      await request.flush();
    }
    await s.close();
    // 文件发送完成
    await request.close();
    // 获取返回数据
    final response = await request.done;
    if (response.statusCode != HttpStatus.ok) {
      return null;
    }
    return dto.key!.trim();
  }

  Future<HttpResult?> _ajax({required String path, dynamic body, GimError? gimError}) async {
    var request = await _httpClient.postUrl(Uri.parse('${_ssl ? 'https' : 'http'}://$_domain$path'));
    request.headers.set(HttpHeaders.contentTypeHeader, "application/json; charset=UTF-8");
    request.headers.set("token", _token!);
    request.headers.set('appId', _appId!);
    request.headers.set("platform", _platform!);
    if (body != null) {
      request.write(json.encode(body));
    }
    var response = await request.close();
    if (response.statusCode == HttpStatus.ok) {
      String result = await response.transform(utf8.decoder).join();
      dynamic map = json.decode(result);
      if (map['code'] == 'OK') {
        return HttpResult(code: map['code'], message: map['message'], data: map['data']);
      } else {
        errLog(map);
        if (gimError != null) {
          gimError(code: map['code'], msg: map['message']);
        }
      }
      return null;
    } else {
      //报错了
      return null;
    }
  }

  void errLog(dynamic data) {
    if (kDebugMode && _debug) {
      print("GIM_ERR ===>>> $data");
    }
  }

  void infoLog(dynamic data) {
    if (kDebugMode && _debug) {
      print("GIM_INFO ===>>> $data");
    }
  }
}
